/*jslint sub: true */
/*global setTimeout, fs, cli, path, DEBUGGER, Class, Deferred, Jedi */

/**
 * Загрузчик конфигураций.
 *
 * @class
 * @name Jedi.ConfigLoader
 * @extends Class
 */
Jedi.ConfigLoader = Class.$extend({

    /**
     * Объект кеширования.
     *
     * @private
     * @field
     * @name Jedi.ConfigLoader#cache
     * @see Jedi.ConfigLoader#getCache
     * @type {Object|null}
     */
    cache : null,

    /**
     * Объект блокировок.
     *
     * @private
     * @field
     * @name Jedi.ConfigLoader#locks
     * @see Jedi.ConfigLoader#getLocks
     * @type {Object|null}
     */
    locks : null,

    /**
     * Объект очереди.
     *
     * @private
     * @field
     * @name Jedi.ConfigLoader#delay
     * @see Jedi.ConfigLoader#getDelay
     * @type {|null}
     */
    delay : null,

    /**
     * Время жизни кеша.
     *
     * @private
     * @field
     * @name Jedi.ConfigLoader#ttl
     * @see Jedi.ConfigLoader#getTtl
     * @type {Number|null}
     */
    ttl : null,

    /**
     * Признак, что необходимо использовать отладчик.
     *
     * @private
     * @field
     * @name Jedi.ConfigLoader#useDebugger
     * @type {Boolean|null}
     */
    useDebugger : null,

    /**
     * Получить время жизни кеша.
     *
     * @protected
     * @function
     * @name Jedi.ConfigLoader#getTtl
     * @see Jedi.ConfigLoader#ttl
     * @return {Number}
     */
    getTtl : function () {
        'use strict';
        return this.ttl;
    },

    /**
     * Получить объект кеширования.
     *
     * @protected
     * @function
     * @name Jedi.ConfigLoader#getCache
     * @see Jedi.ConfigLoader#cache
     * @type {Object|null}
     */
    getCache : function () {
        'use strict';
        if (this.cache === null) {
            this.cache = {};
        }
        return this.cache;
    },

    /**
     * Получить объект блокировок.
     *
     * @protected
     * @function
     * @name Jedi.ConfigLoader#getLocks
     * @see Jedi.ConfigLoader#locks
     * @type {Object|null}
     */
    getLocks : function () {
        'use strict';
        if (this.locks === null) {
            this.locks = {};
        }
        return this.locks;
    },

    /**
     * Получить объект очереди.
     *
     * @protected
     * @function
     * @name Jedi.ConfigLoader#getDelay
     * @see Jedi.ConfigLoader#delay
     * @type {Object|null}
     */
    getDelay : function () {
        'use strict';
        if (this.delay === null) {
            this.delay = {};
        }
        return this.delay;
    },

    /**
     * Конструктор.
     *
     * @public
     * @function
     * @name Jedi.ConfigLoader#$init
     * @param {Number} [ttl=0] Время жизни кеша.
     * @param {Boolean} useDebugger Признак, что необходимо использовать отладчик.
     * @return {void}
     */
    $init : function (ttl, useDebugger) {
        'use strict';
        this.ttl = 0;
        // Устанавливаем время жизни кеша.
        if (['undefined'].indexOf(typeof ttl) !== -1) {
            ttl = parseInt(String(ttl), 10);
            if (ttl > 0) {
                this.ttl = ttl;
            }
        }
        // Признак, что необходимо использовать отладчик.
        this.useDebugger = !!useDebugger;
    },

    /**
     * Загрузка.
     *
     * @public
     * @function
     * @name Jedi.ConfigLoader#load
     * @param {String} pathname Путь к конфигурации.
     * @param {Function} callback Обработчик выполнения.
     * @return {void}
     */
    load : function (pathname, callback) {
        'use strict';
        var self = this,
            time = new Date(),
            realpath,
            config,
            cache,
            locks,
            delay;
        function difference(start, end) {
            var startTimestamp = Number(start),
                endTimestamp = Number(end),
                timeDifference = ('  ' + String((endTimestamp - startTimestamp) / 1000 + 0.0001)).split('.'),
                major = timeDifference[0].substring(-3),
                minor = timeDifference[1].substring(0, 3);
            return major + '.' + minor + ' sec: ';
        }
        // Проверяем, что задан путь к конфигурации.
        if (!pathname) {
            throw new TypeError('Pathname not defined');
        }
        // Проверяем, что задан обработчик.
        if (typeof callback !== 'function') {
            throw new TypeError('Callback is not a function');
        }
        pathname = String(pathname || '');
        cache    = this.getCache();
        locks    = this.getLocks();
        delay    = this.getDelay();
        if (cache[pathname]) {
            if (DEBUGGER && self.useDebugger) {
                // Выводим отладочную информацию в консоль.
                cli.debug('==> Load config ' + JSON.stringify(pathname));
            }
            // Конфигурация имеется в кеше.
            (function (cache) {
                setTimeout(function () {
                    if (DEBUGGER && self.useDebugger) {
                        // Выводим отладочную информацию в консоль.
                        cli.debug(difference(time, new Date()) + 'Complete!');
                    }
                    callback(undefined, cache);
                }, 1);
            }(cache[pathname]));
        } else {
            if (locks[pathname]) {
                // Создаем очередь, если не создана.
                if (!(delay[pathname] instanceof Array)) {
                    delay[pathname] = [];
                }
                // Добавляем получение конфигурации в очередь на исполнение,
                // процесс чтения той же самой конфигурации уже работает.
                delay[pathname].push(function () {
                    self.load(pathname, callback);
                });
            } else {
                if (DEBUGGER && self.useDebugger) {
                    // Выводим отладочную информацию в консоль.
                    cli.debug('==> Load config ' + JSON.stringify(pathname));
                }
                locks[pathname] = true;
                Deferred.$create([
                    // Получаем абсолютный путь к файлу конфигурации.
                    function (success, failure) {
                        var time = new Date();
                        fs.realpath(pathname, function (error, resolved) {
                            if (DEBUGGER && self.useDebugger) {
                                // Выводим отладочную информацию в консоль.
                                cli.debug(difference(time, new Date()) + 'fs.realpath: ' + JSON.stringify(pathname));
                            }
                            if (error) {
                                failure(error);
                            } else {
                                realpath = resolved;
                                success();
                            }
                        });
                    },
                    // Получаем содержимое файла конфигурации.
                    function (success, failure) {
                        var time = new Date();
                        fs.readFile(realpath, function (error1, content) {
                            if (DEBUGGER && self.useDebugger) {
                                // Выводим отладочную информацию в консоль.
                                cli.debug(difference(time, new Date()) + 'fs.readFile: ' + JSON.stringify(pathname));
                            }
                            if (error1) {
                                failure(error1);
                            } else {
                                try {
                                    config = JSON.parse(content) || {};
                                    success();
                                } catch (error2) {
                                    failure(error2['message']);
                                }
                            }
                        });
                    },
                    // Подключаем зависимые файлы конфигураций.
                    function (success, failure) {
                        var arrays, objects, deferred;
                        // Очередь отложенных действий.
                        deferred = Deferred.$create([], function (error) {
                            if (error) {
                                failure(error['message']);
                            } else {
                                success();
                            }
                        });
                        // Функция разбора массивов.
                        arrays = function (config) {
                            var index,
                                length = config.length;
                            for (index = 0; index < length; index += 1) {
                                if (typeof config[index] === 'object' && config[index] !== null) {
                                    if (config[index] instanceof Array) {
                                        arrays(config[index]);
                                    } else {
                                        objects(config[index]);
                                    }
                                }
                            }
                        };
                        // Функция разбора объектов.
                        objects = function (config) {
                            var property,
                                length,
                                index;
                            // Функция сращивания двух объектов.
                            function merge(object1, object2) {
                                var property;
                                for (property in object2) {
                                    if (object2.hasOwnProperty(property)) {
                                        if (typeof object1[property] === 'object' && object1[property] && !(object1[property] instanceof Array) && typeof object2[property] === 'object' && object2[property] && !(object2[property] instanceof Array)) {
                                            merge(object1[property], object2[property]);
                                        } else {
                                            object1[property] = object2[property];
                                        }
                                    }
                                }
                            }
                            // Функция добавления в очередь
                            // чтения вложенной конфигурации.
                            function add(element, object) {
                                deferred.add(function (success, failure) {
                                    var dirname = path.dirname(realpath);
                                    try {
                                        self.load(path.join(dirname, String(element || '')),
                                            function (error, config) {
                                                var property;
                                                if (error) {
                                                    failure(error);
                                                } else {
                                                    merge(object, config);
                                                    success();
                                                }
                                            });
                                    } catch (error) {
                                        failure(error['message']);
                                    }
                                });
                            }
                            for (property in config) {
                                if (config.hasOwnProperty(property)) {
                                    if (property === 'include') {
                                        if (config['include'] instanceof Array) {
                                            length = config['include'].length;
                                            for (index = 0; index < length; index += 1) {
                                                add(config['include'][index], config);
                                            }
                                        }
                                        delete config['include'];
                                    } else if (typeof config[property] === 'object' && config[property] !== null) {
                                        if (config[property] instanceof Array) {
                                            arrays(config[property]);
                                        } else {
                                            objects(config[property]);
                                        }
                                    }
                                }
                            }
                        };
                        config = config || {};
                        if (config instanceof Array) {
                            arrays(config);
                        } else {
                            objects(config);
                        }
                        deferred.run();
                    }
                ], function (error) {
                    var index,
                        length;
                    if (DEBUGGER && self.useDebugger) {
                        // Выводим отладочную информацию в консоль.
                        cli.debug(difference(time, new Date()) + 'Complete!');
                    }
                    if (error) {
                        // Выполнено с ошибками.
                        callback(error['message']);
                    } else {
                        // Сохранение значение в кеше.
                        cache[pathname] = config;
                        // Удаление значения по истечению времени хранения.
                        if (self.getTtl() > 0) {
                            setTimeout(function () {
                                if (cache[pathname]) {
                                    delete cache[pathname];
                                }
                            }, self.getTtl());
                        }
                        // Получение итоговой конфигурации.
                        callback(undefined, config);
                    }
                    // Снимаем блокировку чтения конфигурации.
                    if (locks[pathname]) {
                        delete locks[pathname];
                    }
                    // Удаляем все очереди на чтение конфигурации.
                    if (delay[pathname]) {
                        length = delay[pathname].length;
                        for (index = 0; index < length; index += 1) {
                            setTimeout(delay[pathname][index], 1);
                        }
                        delete delay[pathname];
                    }
                }).run();
            }
        }

    }

});
